接下来的一系列文件将会从源码角度来分析Small的架构以及插件化原理及其实现。
整体架构
Small里面比较核心的类有下面三个:
- Small:接口类,提供用户能使用的各类接口
- Bundle:代表插件类,保存了插件的全部信息
- BundleLauncher:插件加载类,根据加载的不同插件类型,有多个子类,如下图:

初始化
先来看一下宿主 App 中的初始化部分,主要在 Application 和 LaunchActivity 中进行。我们把在 Application 处理的称为第一阶段,在 LaunchActivity 中进行的称为第二阶段和第三阶段。
第一阶段:预处理
1 | public class Application extends android.app.Application { |
在 Application 构造函数中调用了 Small.preSetUp(this) 来进行一些设置的工作:
1 | public static void preSetUp(Application context) { |
首先注册了一些默认的 BundleLauncher,保存在 sBundleLaunchers 静态变量中。然后调用 Bundle.onCreateLaunchers(context) 来调用 BundleLauncher.onCreate()方法。
在几个 BundleLauncher 的子类中,ApkBundleLauncher 重新实现了 onCreate()方法。
1 |
|
InstrumentationWrapper 继承自 Instrumentation 并覆盖了下面几个方法:
1 | execStartActivity() |
为什么说替换 Instrumentation 对象是重头戏呢?这里我们先了解一些这个类。
先看一下官方文档对这个类的解释,该类跟踪 Application 及 Activity 的整个生命周期,它的一些方法在 Application 及 Activity 所有生命周期函数的调用中,都会先调用这些方法,因此,得到了这个对象,我们就可以进入并跟踪 Application 和 Activity 的生命周期流程。
Small 想要做到动态注册 Activity,首先在宿主 Manifest 中注册一个命名特殊的占坑 Activity 来欺骗 startActivityForResult 以获得生命周期,再欺骗 performLaunchActivity 来获得插件 Activity 实例,又为了处理之间的信息传递,因此有了后面的 ActivityThreadHandlerCallback。
我们可以在 small/src/main/AndroidManifest.xml 中找到这些占坑位的 Activity: A、A1、A2….A33等。
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
所作的这一切都是为了实现动态注册 Activity,如果你把插件里面的 Activity 都在宿主的 AndroidManifest.xml 文件里面都申明一下,那么上面的这些 Hook 的工作就可以省去了。
这也就是 Small 插件化的基本原理,该原理部分后面会有博客详细介绍。
第二阶段:加载插件
一些配置工作
在 LaunchActivity 的 onStart() 方法中调用了 Small.setUp()。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class LaunchActivity extends Activity {
protected void onStart() {
...
Small.setUp(LaunchActivity.this, new net.wequick.small.Small.OnCompleteListener() {
public void onComplete() {
long tEnd = System.nanoTime();
se.putLong("setUpFinish", tEnd).apply();
long offset = tEnd - tStart;
if (offset < MIN_INTRO_DISPLAY_TIME) {
// 这个延迟仅为了让 "Small Logo" 显示足够的时间, 实际应用中不需要
getWindow().getDecorView().postDelayed(new Runnable() {
public void run() {
// 启动main插件
Small.openUri("main", LaunchActivity.this);
finish();
}
}, (MIN_INTRO_DISPLAY_TIME - offset) / 1000000);
} else {
Small.openUri("main", LaunchActivity.this);
finish();
}
}
});
}
在 Small.setUp() 方法内部主要调用了 Bundle.loadLaunchableBundles(listener)。
1 | protected static void loadLaunchableBundles(Small.OnCompleteListener listener) { |
由于我们注册了了 Small.OnCompleteListener,这里会开启一个线程来调用 loadBundles() 方法。
1 | private static void loadBundles(Context context) { |
在 loadBundles() 方法中首先会解析 bundle.json 数据,这个数据可能会保存在三个地方,它们的读取是有优先级的,SharedPreferences缓存>App DATA File>Assets。
然后调用 setupLaunchers() 设置前面在 preSetup() 方法中注册的几个 BundleLauncher。
1 | protected static void setupLaunchers(Context context) { |
我们分别看一下这几个 BundleLauncher 的 setUp() 方法都做了什么工作:
- ActivityLauncher.setUp()
这里是将在宿主App里面注册的 Activity 添加到 sActivityClasses 中去,这里包括了 app、app+stub、small下面 AndroidMenifest.xml里面注册的 Activity,当然就包括了前面说的占坑位的几个 Activity。
1 |
|
- ApkBundleLauncher.setUp()
这里是对通过动态代理对所有经过 TaskStackBuilder 创建的 PendingIntent 进行 Hook,调用 wrapIntent 用占坑 Activity 来代替真正的 Activity。
另外还有个方法 Small.wrapIntent(Intent),不是通过TaskStackBuilder 创建的 PendingIntent 需要调用这个方法来进行处理。
1 |
|
- WebBundleLauncher.setUp()
看到注释这样解释:在android 7.0以后的版本中,当第一次创建WebView的时候,它会用WebView的Assets路径替换掉原Application Assets路径,这里就提前在这里先创建一个WebView来避免这个事件的发生。
1 |
|
在 setupLaunchers(context) 方法执行完以后,就会调用 loadBundles(manifest.bundles) 方法来加载插件。
加载插件
先来看一下 Bundle.loadBundles(List<Bundle> bundles)方法,这个方法的主要工作就是在注册的所有 BundleLauncher 中为 bundles 列表中的所有 Bundle 找到适合它们的 BundleLauncher,
1 | private static void loadBundles(List<Bundle> bundles) { |
prepareForLaunch()
我们先来看一下 Bundle.prepareForLaunch() 方法,这里是要在 sBundleLaunchers 中为当前的 Bundle 找到一个合适的 BundleLauncher 并赋值给 mApplicableLauncher,并开始解析插件。
1 | protected void prepareForLaunch() { |
这里又分别调用了 sBundleLaunchers 中各个 BundleLauncher 的 resolveBundle() 方法。
1 | public boolean resolveBundle(Bundle bundle) { |
各个 BundleLauncher 都分别重新实现了 preloadBundle(bundle) 和 loadBundle(bundle) 方法,我们分别来看一下。
ActivityLauncher
1 |
|
这里在 mPackageName 为 main 时才会返回true,ActivityLauncher 是用来启动宿主 Activity 的,它并没有实现 loadBundle 方法,因此就算 preloadBundle()方法返回true,它也不会有任何处理的。
SoBundleLauncher.preloadBundle()
因为 ApkBundleLauncher 没有覆盖 preloadBundle() 方法,那么就到了它的父类 SoBundleLauncher.preloadBundle()方法。
1 |
|
插件的解析由 BundleParser 类来完成,不再详述,可以自己分析源码。
ApkBundleLauncher.loadBundle()
为插件创建 LoadedApk 对象,加载dex文件以及lib库,提取Activity并放入sLoadedActivities列表,收集intentFilter并存入sLoadedIntentFilters列表。
1 |
|
AssetBundleLauncher.loadBundle()
WebBundleLauncher 的 loadBundle() 方法也由它的父类 AssetBundleLauncher 来处理,由于 AssetBundleLauncher 是继承自 SoBundleLauncher,因此 preloadBundle() 也由 SoBundleLauncher 处理。
这个方法主要是将插件文件路径转化为index文件路径1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void loadBundle(Bundle bundle) {
String packageName = bundle.getPackageName();
// 获取插件路径
File unzipDir = new File(getBasePath(), packageName);
// 获取indexfile文件,WebBundleLauncher就是在unzipDir后面加上/index.html
File indexFile = new File(unzipDir, getIndexFileName());
// Prepare index url
String uri = indexFile.toURI().toString();
if (bundle.getQuery() != null) {
uri += "?" + bundle.getQuery();
}
URL url;
try {
url = new URL(uri);
} catch (MalformedURLException e) {
Log.e(TAG, "Failed to parse url " + uri + " for bundle " + packageName);
return;
}
String scheme = url.getProtocol();
if (!scheme.equals("http") &&
!scheme.equals("https") &&
!scheme.equals("file")) {
Log.e(TAG, "Unsupported scheme " + scheme + " for bundle " + packageName);
return;
}
bundle.setURL(url);
}
BundleLauncher.postSetUp()
这里也会调用 BundleLauncher 各个子类的 BundleLauncher方法。
但是仅有 ApkBundleLauncher 覆盖了基类的空实现。
1 |
|
至此,插件的初始化部分介绍完毕。